import type { SerialTaskExecuteContext } from '../handler'

import { SerialHandler } from '../handler'

class AudioActuator extends SerialHandler<AudioBuffer, never> {
  private static FADE_DURATION = 0.018
  private static MUTE_VOLUME = 0
  private static UNMUTE_VOLUME = 1

  private audioContext: AudioContext = new AudioContext()

  private bufferSource: AudioBufferSourceNode | null = null

  private gainNode: GainNode | null = null

  private isMute = false
  public execute(
    context: SerialTaskExecuteContext<AudioBuffer, never>,
  ): void {
    if (context.isLastExecute) {
      this.taskCompletedCallback()
      return
    }

    this.bufferSource = this.audioContext.createBufferSource()
    this.bufferSource.buffer = context.taskItem.original!

    this.gainNode = this.audioContext.createGain()
    this.bufferSource.connect(this.gainNode)
    this.gainNode.connect(this.audioContext.destination)

    if (this.isMute) {
      this.gainNode.gain.setValueAtTime(AudioActuator.MUTE_VOLUME, this.audioContext.currentTime)
    }
    else {
      this.gainNode.gain.setValueAtTime(AudioActuator.MUTE_VOLUME, this.audioContext.currentTime)
      this.gainNode.gain.linearRampToValueAtTime(AudioActuator.UNMUTE_VOLUME, this.audioContext.currentTime + AudioActuator.FADE_DURATION)

      this.gainNode.gain.setValueAtTime(AudioActuator.UNMUTE_VOLUME, this.audioContext.currentTime + context.taskItem.original!.duration - AudioActuator.FADE_DURATION)
      this.gainNode.gain.linearRampToValueAtTime(AudioActuator.MUTE_VOLUME, this.audioContext.currentTime + context.taskItem.original!.duration)
    }

    this.bufferSource.start()
    this.bufferSource.addEventListener('ended', () => {
      this.taskCompletedCallback()
    })
  }

  protected onFinish(): void {
    if (this.bufferSource) {
      this.bufferSource.stop()
      this.bufferSource = null
    }
    if (this.gainNode) {
      this.gainNode.disconnect()
      this.gainNode = null
    }
    if (this.audioContext) {
      this.audioContext.suspend()
      this.audioContext = new AudioContext()
    }
    this.executeController?.$bus.emit('_audioActuatorFinish')
  }

  protected onBeforeFirstExecute(): void {
    this.executeController?.$bus.emit('_audioActuatorBeforeFirstExecute')
  }

  public mute(): void {
    if (this.gainNode) {
      this.gainNode.gain.setValueAtTime(AudioActuator.UNMUTE_VOLUME, this.audioContext.currentTime)
      this.gainNode.gain.linearRampToValueAtTime(AudioActuator.MUTE_VOLUME, this.audioContext.currentTime + AudioActuator.FADE_DURATION)
    }
    this.isMute = true
  }

  public unmute(): void {
    if (this.gainNode) {
      this.gainNode.gain.setValueAtTime(AudioActuator.MUTE_VOLUME, this.audioContext.currentTime)
      this.gainNode.gain.linearRampToValueAtTime(AudioActuator.UNMUTE_VOLUME, this.audioContext.currentTime + AudioActuator.FADE_DURATION)
    }
    this.isMute = false
  }
}

export default AudioActuator
